package alibaba

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"

	log "golang.aliyun-inc.com/agit/alog/alogcore"
)

// API interface for agitflow
type API interface {
	// QueryUser query user from code biz platform
	QueryUser(ctx context.Context, keyID string) (*UserInfoResp, error)
	// QueryMR query MR from code biz platform
	QueryMR(ctx context.Context, mrID string) ([]*MergeRequestResp, error)
	// CreateMR create MR from code biz platform
	CreateMR(ctx context.Context, params *AGitFlowParams) (*MergeRequestResp, error)
	// UpdateMR update MR from code biz platform
	UpdateMR(ctx context.Context, params *AGitFlowParams) (*MergeRequestResp, error)
	// UpdateMRState move MR from creating to opened or canceled
	UpdateMRState(ctx context.Context, params *UpdateMRStateParams) (*MergeRequestResp, error)
	// QueryMRs query mr by projectId and authorId
	QueryMRs(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error)
}

type CodeReviewAPI interface {
	// QueryUser query user from code biz platform
	QueryUser(ctx context.Context, keyID string) (*UserInfoResp, error)
	// QueryCR query CR from code biz platform
	QueryCR(ctx context.Context, projectID, localID string) ([]*CodeReviewBasicInfo, error)
	// CreateCR create a change request from code biz platform
	CreateCR(ctx context.Context, params *CodeReviewRequest) (*CodeReviewBasicInfo, error)
	// UpdateCR update CR from code biz platform
	UpdateCR(ctx context.Context, params *CodeReviewRequest) (*CodeReviewBasicInfo, error)
	// QueryCRs query mr by projectId and authorId
	QueryCRs(ctx context.Context, queryReq *QueryCodeReviewReq) ([]*CodeReviewBasicInfo, error)
	// GenerateLocalID generate the next CR's local id in a project
	GenerateLocalID(ctx context.Context, projectID string) (int64, error)
	// QueryPatch query given cr's latest patch
	QueryPatch(ctx context.Context, projectID, crID string) (*CodeReviewPatchSet, error)
	// CreateRefCR create CR for branch and tags
	CreateRefCR(ctx context.Context, request *RefReviewRequest) (*CodeReviewBasicInfo, error)
	// QueryRefReviewSetting query ref review setting
	QueryRefReviewSetting(ctx context.Context, projectId string) (*RefReviewSetting, error)
}

// CodeupAPI struct
type CodeupAPI struct {
	apiURL              string
	appKey              string
	appSecret           string
	externUID           string
	epQueryUser         string
	epQueryMR           string
	epMergeRequest      string
	epUpdateMRState     string
	epUpdateMRTBD       string
	epQueryMRs          string
	epCreateCR          string
	epQueryCR           string
	epUpdateCR          string
	epGenerateLocalID   string
	epQueryPatch        string
	epCreateRefReviewCR string
	epQueryRefSetting   string
	client              *http.Client
}

// NewCodeupAPI create a codeup API instance
func NewCodeupAPI(apiURL, appKey, appSecret, externUID string) *CodeupAPI {
	return &CodeupAPI{
		apiURL:              apiURL,
		appKey:              appKey,
		appSecret:           appSecret,
		externUID:           externUID,
		epQueryUser:         "/api/v3/internal/userinfo",
		epQueryMR:           "/api/v3/internal/merge_requests",
		epUpdateMRState:     "/api/v3/internal/movemr",
		epMergeRequest:      "/api/v3/internal/codereview",
		epUpdateMRTBD:       "/api/v3/internal/tbd/codereview",
		epQueryMRs:          "/api/v3/internal/tbd/merge_requests",
		epQueryCR:           "/api/v3/internal/code_reviews",
		epCreateCR:          "/api/v3/internal/code_review",
		epUpdateCR:          "/api/v3/internal/code_review/update",
		epGenerateLocalID:   "/api/v3/internal/code_review/generate-id",
		epQueryPatch:        "/api/v3/internal/code_review/patch",
		epCreateRefReviewCR: "/api/v3/internal/code_review/ref",
		epQueryRefSetting:   "/api/v3/internal/ref_review/setting",
		client: &http.Client{
			Transport: &http.Transport{
				MaxIdleConns:       10,
				IdleConnTimeout:    30 * time.Second,
				DisableCompression: true,
			},
		},
	}
}

// CodeupApiErrorResponse struct
type CodeupApiErrorResponse struct {
	Cause     string `json:"cause,omitempty"`
	ErrorCode string `json:"errorCode,omitempty"`
	Message   string `json:"message,omitempty"`
	Status    bool   `json:"status,omitempty"`
	TraceID   string `json:"traceId,omitempty"`
}

// QueryUser query codeup user by key id
func (a *CodeupAPI) QueryUser(ctx context.Context, keyID string) (*UserInfoResp, error) {
	rawURL := a.apiURL + a.epQueryUser
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}
	urlWithParams := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s&key_id=%s",
		parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.appSecret, keyID)
	payload := &UserInfoResp{}
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, payload); err != nil {
		return nil, err
	}

	return payload, nil
}

// QueryMR query a merge request by id
func (a *CodeupAPI) QueryMR(ctx context.Context, mrID string) ([]*MergeRequestResp, error) {
	rawURL := a.apiURL + a.epQueryMR
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}
	urlWithParams := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s&%s=%s",
		parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.systemToken(), "ids", mrID)

	var mrRsp []*MergeRequestResp
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, &mrRsp); err != nil {
		return nil, err
	}

	return mrRsp, nil
}

// CreateMR Create a MR by API invoking
func (a *CodeupAPI) CreateMR(ctx context.Context, params *AGitFlowParams) (*MergeRequestResp, error) {
	// validate input first
	if params.GlID == "" {
		return nil, fmt.Errorf("Empty GL_ID, won't call API")
	}

	if params.ProjectID == "" {
		return nil, fmt.Errorf("Empty GL_REPOSITORY_ID, won't call API")
	}

	if params.NewOID == "" {
		return nil, fmt.Errorf("Empty source commit sha, won't call API")
	}

	rawURL := a.apiURL + a.epMergeRequest
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}
	urlWithAuth := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s", parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.systemToken())
	mr := convert2MrRequest(params)
	mrResp := &MergeRequestResp{}

	if err := a.doRequest(ctx, urlWithAuth, "POST", mr, mrResp); err != nil {
		return nil, err
	}

	return mrResp, nil
}

// UpdateMR is  a MR by API invoking
func (a *CodeupAPI) UpdateMR(ctx context.Context, params *AGitFlowParams) (*MergeRequestResp, error) {
	// validate input first
	if params.GlID == "" {
		return nil, fmt.Errorf("empty GL_ID, won't call API")
	}

	if params.ProjectID == "" {
		return nil, fmt.Errorf("Empty GL_REPOSITORY_ID, won't call API")
	}

	if params.NewOID == "" {
		return nil, fmt.Errorf("Empty source commit sha, won't call API")
	}

	var parsedURL *url.URL
	var err error
	var requestBody []byte
	var req *http.Request
	var rsp *http.Response
	var rawURL string

	if params.PushOpt == "update" {
		rawURL = a.apiURL + a.epUpdateMRTBD + "/" + params.ID
	} else {
		rawURL = a.apiURL + a.epMergeRequest + "/" + params.ID
	}

	if parsedURL, err = url.Parse(rawURL); err != nil {
		return nil, err
	}
	urlWithAuth := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s", parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.systemToken())

	mr := convert2MrRequest(params)
	if requestBody, err = json.Marshal(mr); err != nil {
		return nil, err
	}
	log.Info(ctx, "update MR", log.NewStringField("requestBody", string(requestBody)))
	if req, err = http.NewRequest("PUT", urlWithAuth, strings.NewReader(string(requestBody))); err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	log.Info(ctx, "begin calling codeup", log.NewStringField("url", urlWithAuth))
	if rsp, err = a.client.Do(req); err != nil {
		log.WithError(err).Error(ctx, "call codeup api failed")
		return nil, fmt.Errorf("call update mr api failed")
	}
	defer rsp.Body.Close()

	// If action is UPDATE and rsp code is 409, means another user already update the CR
	if rsp.StatusCode == 409 {
		mrIDTip := params.ID
		if mrIDTip == "" {
			mrIDTip = "<CR-ID>"
		}
		var exceptionResp UpdateMRExceptionResp
		err = json.NewDecoder(rsp.Body).Decode(&exceptionResp)
		if err != nil {
			return nil, err
		}
		// trunk based style conflict message
		if exceptionResp.Cause != "" && strings.HasPrefix(exceptionResp.Cause, "Merge request #") {
			return nil, fmt.Errorf(exceptionResp.Cause)
		}
		return nil, fmt.Errorf("Remote CR has been updated!\n"+
			"Please fetch the latest patch by executing: `git download %s` and rewrite your code.\n"+
			"If your work is done, executing `git pr --change %s` to upload your new patch", mrIDTip, mrIDTip)
	}
	if rsp.StatusCode >= 300 {
		rspBytes, err := ioutil.ReadAll(rsp.Body)
		if err != nil {
			return nil, err
		}
		return nil, fmt.Errorf("response with code: %d, msg: %s", rsp.StatusCode, rspBytes)
	}

	var mrResp MergeRequestResp
	if err = json.NewDecoder(rsp.Body).Decode(&mrResp); err != nil {
		return nil, err
	}
	log.Info(ctx, "update MR resp", log.NewStringField("resp", fmt.Sprintf("%+v", mrResp)))
	return &mrResp, nil
}

// UpdateMRState update the status of a mr
func (a *CodeupAPI) UpdateMRState(ctx context.Context, params *UpdateMRStateParams) (*MergeRequestResp, error) {
	// validate input first
	if params.AGitFlowParams.GlID == "" {
		return nil, fmt.Errorf("Empty GL_ID, won't call API")
	}

	if params.AGitFlowParams.ProjectID == "" {
		return nil, fmt.Errorf("Empty GL_REPOSITORY_ID, won't call API")
	}

	if params.AGitFlowParams.NewOID == "" {
		return nil, fmt.Errorf("Empty source commit sha, won't call API")
	}

	rawURL := a.apiURL + a.epUpdateMRState
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}
	urlWithAuth := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s", parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.systemToken())

	mrResp := &MergeRequestResp{}
	if err := a.doRequest(ctx, urlWithAuth, "POST", params, mrResp); err != nil {
		return nil, err
	}
	if mrResp.State == "canceled" {
		return nil, errors.New("error occurred when creating mr, canceled")
	}
	return mrResp, nil
}

// QueryMRs query opened MRs by projectID and authorID
func (a *CodeupAPI) QueryMRs(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error) {
	var err error

	rawURL := a.apiURL + a.epQueryMRs
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}

	urlWithParams := fmt.Sprintf("%s?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
		parsedURL, "app_key", a.appKey, "extern_uid", a.externUID, "system_token", a.systemToken(), "pid", projectID, "author", authorID, "target", targetBranch)

	var mrRsp []*MergeRequestResp
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, &mrRsp); err != nil {
		return nil, err
	}
	return mrRsp, nil
}

func (a *CodeupAPI) systemToken() string {
	data := []byte(fmt.Sprintf("%s%s%s%s", a.externUID, a.appKey, a.appSecret, time.Now().Format("200601021504")))
	sum := md5.Sum(data)
	return hex.EncodeToString(sum[:])
}

func (a *CodeupAPI) QueryCR(ctx context.Context, projectId, localId string) ([]*CodeReviewBasicInfo, error) {
	rawURL := a.apiURL + a.epQueryCR
	urlWithParams := fmt.Sprintf("%s?isOpened=true&%s=%s&%s=%s", rawURL, "projectId", projectId, "localId", localId)

	var crBasicInfo []*CodeReviewBasicInfo
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, &crBasicInfo); err != nil {
		return nil, err
	}
	return crBasicInfo, nil
}

func (a *CodeupAPI) QueryCRs(ctx context.Context, queryReq *QueryCodeReviewReq) ([]*CodeReviewBasicInfo, error) {
	rawURL := a.apiURL + a.epQueryCR
	urlWithParams := fmt.Sprintf("%s?isOpened=true", rawURL)
	if queryReq.ProjectID != "" {
		urlWithParams = urlWithParams + fmt.Sprintf("&projectId=%s", queryReq.ProjectID)
	}
	if queryReq.AuthorID != "" {
		urlWithParams = urlWithParams + fmt.Sprintf("&authorId=%s", queryReq.AuthorID)
	}
	if queryReq.TargetBranch != "" {
		urlWithParams = urlWithParams + fmt.Sprintf("&target_branch=%s", queryReq.TargetBranch)
	}
	if queryReq.LocalBranch != "" {
		urlWithParams = urlWithParams + fmt.Sprintf("&local_branch=%s", queryReq.LocalBranch)
	}

	var crBasicInfo []*CodeReviewBasicInfo
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, &crBasicInfo); err != nil {
		return nil, err
	}
	return crBasicInfo, nil
}

func (a *CodeupAPI) CreateCR(ctx context.Context, codeReviewRequest *CodeReviewRequest) (*CodeReviewBasicInfo, error) {
	var err error
	rawURL := a.apiURL + a.epCreateCR
	crResp := &CodeReviewBasicInfo{}
	if err = a.doRequest(ctx, rawURL, "POST", codeReviewRequest, crResp); err != nil {
		return nil, err
	}
	return crResp, nil
}

func (a *CodeupAPI) UpdateCR(ctx context.Context, params *CodeReviewRequest) (*CodeReviewBasicInfo, error) {
	rawURL := a.apiURL + a.epUpdateCR
	crResp := &CodeReviewBasicInfo{}
	if err := a.doRequest(ctx, rawURL, "POST", params, crResp); err != nil {
		return nil, err
	}
	return crResp, nil
}

func (a *CodeupAPI) GenerateLocalID(ctx context.Context, projectId string) (int64, error) {
	var localId int64

	rawURL := a.apiURL + a.epGenerateLocalID
	endpoint := fmt.Sprintf("%s?%s=%s", rawURL, "projectId", projectId)

	if err := a.doRequest(ctx, endpoint, "POST", nil, &localId); err != nil {
		return 0, err
	}
	return localId, nil
}

func (a *CodeupAPI) QueryPatch(ctx context.Context, projectId, crId string) (*CodeReviewPatchSet, error) {
	rawURL := a.apiURL + a.epQueryPatch
	urlWithParams := fmt.Sprintf("%s?%s=%s&%s=%s", rawURL, "projectId", projectId, "crId", crId)
	patch := &CodeReviewPatchSet{}
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, patch); err != nil {
		return nil, err
	}
	return patch, nil
}

func (a *CodeupAPI) CreateRefCR(ctx context.Context, request *RefReviewRequest) (*CodeReviewBasicInfo, error) {
	rawURL := a.apiURL + a.epCreateRefReviewCR
	basicInfo := &CodeReviewBasicInfo{}
	if err := a.doRequest(ctx, rawURL, "POST", request, basicInfo); err != nil {
		return nil, err
	}
	return basicInfo, nil
}

func (a *CodeupAPI) QueryRefReviewSetting(ctx context.Context, projectId string) (*RefReviewSetting, error) {
	rawURL := a.apiURL + a.epQueryRefSetting
	urlWithParams := fmt.Sprintf("%s?%s=%s", rawURL, "projectId", projectId)
	setting := &RefReviewSetting{}
	if err := a.doRequest(ctx, urlWithParams, "GET", nil, setting); err != nil {
		return nil, err
	}
	return setting, nil
}

func (a *CodeupAPI) doRequest(ctx context.Context, endpoint, method string, requestBody, responseBody interface{}) (err error) {
	var parsedURL *url.URL
	var req *http.Request
	var rsp *http.Response
	var body []byte
	var codeupApiErrorResponse *CodeupApiErrorResponse

	if parsedURL, err = url.Parse(endpoint); err != nil {
		return err
	}
	v := parsedURL.Query()
	v.Set("app_key", a.appKey)
	v.Set("system_token", a.systemToken())
	v.Set("extern_uid", a.externUID)
	parsedURL.RawQuery = v.Encode()
	if requestBody != nil {
		body, err = json.Marshal(requestBody)
		if err != nil {
			log.WithError(err).Error(ctx, "Failed to marshal project object")
			return err
		}
	}
	if req, err = http.NewRequest(method, parsedURL.String(), bytes.NewReader(body)); err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	log.Info(ctx, "begin calling codeup", log.NewStringField("url", parsedURL.String()))
	if rsp, err = a.client.Do(req); err != nil {
		log.WithError(err).Error(ctx, "call code review api failed")
		return fmt.Errorf("call code review api failed")
	}
	defer rsp.Body.Close()
	buf := &bytes.Buffer{}
	reader := io.TeeReader(rsp.Body, buf)
	bodyContent, err := io.ReadAll(reader)
	log.Debug(ctx, "request and body",
		log.NewStringField("request url", req.URL.String()),
		log.NewStringField("response bodyContent", string(bodyContent)))
	if rsp.StatusCode >= 300 {
		rspBytes, err := io.ReadAll(buf)
		if err != nil {
			return err
		}

		err = json.Unmarshal(rspBytes, &codeupApiErrorResponse)
		if err != nil {
			return err
		}
		return fmt.Errorf(codeupApiErrorResponse.Message)
	}

	if err = json.NewDecoder(buf).Decode(responseBody); err != nil {
		return err
	}
	return nil
}
